package info.motteke.swt;

import java.io.InputStream;
import java.util.Timer;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GlyphMetrics;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageLoader;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Resource;
import org.eclipse.swt.graphics.TextLayout;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Caret;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Widget;

/**
 * テキスト中に画像やコントロールを挿入できるテキストです。
 *
 * @author Y.Wakamatsu
 *
 */
public class LayoutText {

    private static final int CARET_WIDTH = 2;

    /** テキスト内で、画像やコントロールと置換される文字です。 */
    public static final char REPLACEMENT = '\uFFFC';

    /** 置換文字の文字列表現。 */
    private static final String REPLACEMENT_STRING = Character.toString(REPLACEMENT);

    /** インデックスが存在しない状態。 */
    private static final int UNINDEXED = -1;

    /** 空の配列。 */
    private static final int[] EMPTY_INT = new int[0];

    /** ディスプレイです。 */
    private final Display display;

    /** シェルです。 */
    private final Shell shell;

    /** このウィジェットのベース。 */
    private final ScrolledComposite base;

    /** テキストエリア。 */
    private final Canvas textarea;

    /** 文字のレイアウト。 */
    private final TextLayout layout;

    /** 置換対象のオフセット。 */
    private int[] offsets = EMPTY_INT;

    /** 置換対象。 */
    private ReplaceEntry[] entries;

    /** テキストを表示するオフセット。 */
    private Point margin = new Point(5, 5);

    /** キャレット。 */
    private final Caret caret;

    /** キャレットの現在位置です。 */
    private int caretOffset;

    /** 現在選択されている範囲の先頭。 */
    private int startSelectionIndex = UNINDEXED;

    /** 現在選択されている範囲の最後。 */
    private int endSelectionIndex = UNINDEXED;

    /** デフォルトの背景色。 */
    private final Color defaultBackground;

    /** 選択されている箇所の文字色。 */
    private final Color selectedForeground;

    /** 選択されている箇所の背景色。 */
    private final Color selectedBackground;

    /** アニメーション用のタイマー。 */
    private Timer timer;

    /**
     * コンストラクタです。
     *
     * @param parent
     *            親となるウィジェット。nullは不可。
     * @param style
     *            このインスタンスのスタイル
     */
    public LayoutText(Composite parent, int style) {
        if (parent == null) {
            throw new NullPointerException("引数parentはnullに出来ません。");
        }

        display = parent.getDisplay();
        shell = parent.getShell();

        // 色の準備
        defaultBackground = display.getSystemColor(SWT.COLOR_WHITE);
        selectedForeground = display.getSystemColor(SWT.COLOR_WHITE);
        selectedBackground = display.getSystemColor(SWT.COLOR_DARK_BLUE);

        // ベースとなるコントロールの作成
        base = new ScrolledComposite(parent, style | SWT.V_SCROLL);
        base.setLayout(new FillLayout());
        base.setExpandHorizontal(true);
        base.setExpandVertical(true);

        // テキストを表示するコントロールの作成
        textarea = new Canvas(base, SWT.DOUBLE_BUFFERED);
        textarea.setBackground(defaultBackground);
        base.setContent(textarea);

        // レイアウトの作成
        layout = new TextLayout(display);

        // リスナの登録
        LayoutTextListener listener = new LayoutTextListener(this);
        textarea.addFocusListener(listener);
        textarea.addKeyListener(listener);
        textarea.addMouseListener(listener);
        textarea.addPaintListener(listener);
        textarea.addMouseMoveListener(listener);
        textarea.addControlListener(listener);

        base.addDisposeListener(listener);

        // キャレットの作成
        caret = new Caret(textarea, SWT.NONE);
        caret.setVisible(false);
    }

    /**
     * 文字列を設定します。
     * 画像や{@linkplain Composite}の埋め込みは{@linkplain #REPLACEMENT}が対象です。
     *
     * @param text
     *            設定する文字列。
     * @see {@linkplain #setImageEntry(Image, int)}
     * @see {@linkplain #setCompositeEntry(Composite, int)}
     */
    public void setText(String text) {
        if (text == null) {
            throw new NullPointerException("引数textはnullに出来ません。");
        }

        layout.setText(text);
        int length = text.length();
        int[] indexes = new int[length];
        int index = 0;

        for (int i = 0; i < length; i++) {
            if (text.charAt(i) == REPLACEMENT) {
                indexes[index++] = i;
            }
        }

        offsets = new int[index];
        System.arraycopy(indexes, 0, offsets, 0, index);

        entries = new ReplaceEntry[index];
    }

    /**
     * 埋め込む画像を設定します。
     *
     * @param image
     *            埋め込む画像
     * @param index
     *            埋め込む位置。0を開始として、{@linkplain #REPLACEMENT}の位置を指定します。
     */
    public void setImageEntry(Image image, int index) {
        int offset = offsets[index];
        entries[index] = new ImageReplaceEntry(layout, margin, offset, image);
        setStyle(image.getBounds(), offset);
    }

    /**
     * タイマーを取得します。
     *
     * @return タイマー
     */
    private Timer getTimer() {
        if (timer == null) {
            timer = new Timer();
        }

        return timer;
    }

    public void setAnimation(InputStream input, int index) {
        ImageLoader loader = new ImageLoader();
        loader.load(input);

        setAnimation(loader, index);
    }

    public void setAnimation(ImageLoader loader, int index) {
        int offset = offsets[index];
        AnimationEntry entry = new AnimationEntry(this, layout, margin, offset, loader, getTimer());
        entries[index] = entry;

        int width = entry.getWidth();
        int height = entry.getHeight();

        setStyle(new Rectangle(0, 0, width, height), offset);
    }

    /**
     * 埋め込む{@link Composite}を設定します。
     *
     * @param image
     *            埋め込む{@linkplain Composite}
     * @param index
     *            埋め込む位置。0を開始として、{@linkplain #REPLACEMENT}の位置を指定します。
     */
    public void setCompositeEntry(Composite c, int index) {
        int offset = offsets[index];
        entries[index] = new CompositeReplaceEntry(layout, margin, offset, c);
        setStyle(c.getBounds(), offset);
    }

    /**
     * 埋め込み文字に対してスタイルを設定します。
     *
     * @param bounds
     *            埋め込まれるオブジェクトの大きさ
     * @param offset
     *            埋め込まれる位置
     */
    private void setStyle(Rectangle bounds, int offset) {
        TextStyle style = new TextStyle(null, null, null);
        style.metrics = new GlyphMetrics(bounds.height, 0, bounds.width);
        layout.setStyle(style, offset, offset);
    }

    private void moveCaret() {
        Rectangle bounds = layout.getBounds(caretOffset, caretOffset);
        caret.setBounds(bounds.x + margin.x, bounds.y + margin.y, CARET_WIDTH, bounds.height);
    }

    /**
     * このテキストの本体となる{@linkplain Composite}を取得します。
     *
     */
    public Composite getComposite() {
        return base;
    }

    /**
     * ディスプレイを取得します。
     *
     * @return ディスプレイ
     */
    public Display getDisplay() {
        return display;
    }

    /**
     * シェルを取得します。
     *
     * @return シェル
     */
    public Shell getShell() {
        return shell;
    }

    /**
     * 文字列の選択を解除します。
     */
    public void cancelSelection() {
        startSelectionIndex = UNINDEXED;
        endSelectionIndex = UNINDEXED;

        for (int i = 0, length = layout.getText().length(); i < length; i++) {
            setUnselected(i);
        }
    }

    /**
     * キャレットを表示します。
     */
    /* package */void showCaret() {
        caret.setVisible(true);
    }

    /**
     * キャレットを非表示にします。
     */
    /* package */void hideCaret() {
        caret.setVisible(false);
    }

    /**
     * キャレットを移動します。
     * 単位は文字になります。
     *
     * @param dir
     *            移動した方向
     * @param select
     *            選択を継続するか
     */
    /* package */void moveCaretByDirection(int dir, boolean select) {

        int diff;

        switch (dir) {
            case SWT.UP:
                diff = getCaretUpDiff();
                break;
            case SWT.DOWN:
                diff = getCaretDownDiff();
                break;
            case SWT.LEFT:
                diff = -1;
                break;
            case SWT.RIGHT:
                diff = 1;
                break;
            default:
                diff = 0;
        }

        caretOffset = normalizeOffset(caretOffset + diff);

        if (select) {
            endSelectionIndex = caretOffset;
        } else {
            startSelectionIndex = caretOffset;
            endSelectionIndex = UNINDEXED;
        }

        moveCaret();
        paintSelection();
    }

    private int getCaretUpDiff() {
        return getCaretDiff(-1);
    }

    private int getCaretDownDiff() {
        return getCaretDiff(1);
    }

    private int getCaretDiff(int dir) {
        int lineIndex = layout.getLineIndex(caretOffset) + dir;
        int lineCount = layout.getLineCount();

        if (lineIndex == -1 || lineCount == lineIndex) {
            return 0;
        }

        Rectangle lineBounds = layout.getLineBounds(lineIndex);
        Rectangle bounds = layout.getBounds(caretOffset, caretOffset);

        int x = bounds.x;
        int y = lineBounds.y;

        int offset = layout.getOffset(x, y, null);

        return offset - caretOffset;
    }

    private int normalizeOffset(int offset) {
        if (offset < 0) {
            return 0;
        }

        int length = layout.getText().length();
        if (length <= offset) {
            return length;
        }

        return offset;
    }

    /**
     * キャレットを移動します。
     * 単位はピクセルです。
     *
     * @param x
     *            現在の選択位置
     * @param y
     *            現在の選択位置
     * @param select
     *            選択を継続するか
     */
    /* package */void moveCaretByPoint(int x, int y, boolean select) {
        int offset = layout.getOffset(x, y, null);
        caretOffset = offset;
        moveCaret();

        if (select) {
            System.out.println("selection extending");
            endSelectionIndex = offset;
        } else {
            System.out.println("selection start");
            startSelectionIndex = offset;
            endSelectionIndex = UNINDEXED;
        }

        paintSelection();
    }

    private void paintSelection() {
        int length = layout.getText().length();
        boolean selectExists = (startSelectionIndex != UNINDEXED) && (endSelectionIndex != UNINDEXED);

        System.out.println("選択範囲を反転します。" + selectExists);
        for (int i = 0; i < length; i++) {
            setUnselected(i);
        }
        if (selectExists) {
            int start = Math.min(startSelectionIndex, endSelectionIndex);
            int end = Math.max(startSelectionIndex, endSelectionIndex);

            for (int i = start; i < end; i++) {
                setSelected(i);
            }
        }

        redraw();
    }

    private void setUnselected(int offset) {
        if (isReplacement(offset)) {
            return;
        }

        TextStyle style;
        TextStyle old = layout.getStyle(offset);

        if (old != null) {
            style = new TextStyle(old);
        } else {
            style = new TextStyle();
        }
        style.foreground = null;
        style.background = null;

        layout.setStyle(style, offset, offset);
    }

    private void setSelected(int offset) {
        if (isReplacement(offset)) {
            return;
        }

        TextStyle style;
        TextStyle old = layout.getStyle(offset);

        if (old != null) {
            style = new TextStyle(old);
        } else {
            style = new TextStyle();
        }
        style.foreground = selectedForeground;
        style.background = selectedBackground;

        layout.setStyle(style, offset, offset);
    }

    private boolean isReplacement(int offset) {
        for (int o : offsets) {
            if (o < offset) {
                return false;
            }
            if (o == offset) {
                return true;
            }
        }

        return false;
    }

    /**
     * テキストを全選択します。
     */
    public void selectAllText() {
        int length = layout.getText().length();
        startSelectionIndex = 0;
        endSelectionIndex = length - 1;

        for (int i = 0; i < length; i++) {
            setSelected(i);
        }
    }

    /**
     * 選択範囲のテキストを取得します。
     *
     * @return 選択範囲のテキスト。
     */
    public String getSelectedText() {
        return getSelectedText(" ");
    }

    /**
     * 選択範囲のテキストを取得します。
     *
     * @param replacement
     *            画像等を置き換える文字
     * @return 選択範囲のテキスト
     */
    public String getSelectedText(String replacement) {
        String text = layout.getText();
        return text.substring(startSelectionIndex, endSelectionIndex)
                   .replaceAll(REPLACEMENT_STRING, replacement);
    }

    /**
     * レイアウトデータを設定します。
     *
     * @param layoutData
     *            レイアウトデータ
     * @see {@linkplain Composite#setLayoutData(Object)}
     */
    public void setLayoutData(Object layoutData) {
        base.setLayoutData(layoutData);
    }

    /**
     * 描画を行います。
     */
    public void redraw() {
        if (!base.isDisposed() && !textarea.isDisposed()) {
            base.redraw();
            textarea.redraw();
        }
    }

    /**
     * マージンを設定します。
     */
    public void setMargin(int x, int y) {
        setMargin(new Point(x, y));
    }

    /**
     * マージンを設定します。
     */
    public void setMargin(Point margin) {
        this.margin = margin;
    }

    /**
     * マージンを取得します。
     */
    public Point getMargin() {
        return margin;
    }

    public void setBackground(Color background) {
        textarea.setBackground(background);
    }

    /**
     * このテキストを破棄します。
     */
    public void dispose() {
        dispose(textarea);
        dispose(caret);
        dispose(layout);
        dispose(base);
        dispose(defaultBackground);
    }

    /**
     * リソースを破棄します。
     *
     * @param resource
     *            破棄するリソース
     */
    private void dispose(Resource resource) {
        if (!resource.isDisposed()) {
            resource.dispose();
        }
    }

    /**
     * ウィジェットを破棄します。
     *
     * @param widget
     *            破棄するウィジェット
     */
    private void dispose(Widget widget) {
        if (!widget.isDisposed()) {
            widget.dispose();
        }
    }

    private class LayoutTextListener implements MouseListener, MouseMoveListener, KeyListener,
                                                  FocusListener, PaintListener, ControlListener,
                                                  DisposeListener {

        private static final int MOUSE_LEFT = 1;

        @SuppressWarnings("unused")
        private static final int MOUSE_CENTER = 2;

        private static final int MOUSE_RIGHT = 3;

        private final LayoutText text;

        private boolean mousePressing = false;

        private boolean dragging = false;

        private boolean shiftEnabled = false;

        /* package */public LayoutTextListener(LayoutText text) {
            this.text = text;
        }

        @Override
        public void mouseDoubleClick(MouseEvent e) {
            /* empty */
        }

        @Override
        public void mouseDown(MouseEvent e) {
            mousePressing = true;
            switch (e.button) {
                case MOUSE_LEFT:
                    mouseLeftPressed(e);
                    break;

                case MOUSE_RIGHT:
                    mouseRightPressed(e);
                    break;
            }
        }

        private void mouseLeftPressed(MouseEvent e) {
            if (!shiftEnabled) {
                System.out.println("左クリック押下");
                text.cancelSelection();
                text.moveCaretByPoint(e.x, e.y, false);
            }
        }

        private void mouseRightPressed(MouseEvent e) {
            /* empty */
        }

        @Override
        public void mouseUp(MouseEvent e) {
            switch (e.button) {
                case MOUSE_LEFT:
                    mouseLeftReleased(e);
                    break;

                case MOUSE_RIGHT:
                    mouseRightReleased(e);
            }
            mousePressing = false;
            dragging = false;
        }

        private void mouseLeftReleased(MouseEvent e) {
            if (shiftEnabled) {
                System.out.println("左クリック解放");
                text.moveCaretByPoint(e.x, e.y, dragging | shiftEnabled);
            }
        }

        private void mouseRightReleased(MouseEvent e) {
            // TODO 右クリックメニューの表示
        }

        @Override
        public void mouseMove(MouseEvent e) {
            if (mousePressing) {
                text.moveCaretByPoint(e.x, e.y, true);
                dragging = true;
            }
        }

        @Override
        public void keyPressed(KeyEvent e) {
            boolean select = isShiftEnabled(e);

            switch (e.keyCode) {
                case SWT.SHIFT:
                    enableShift();
                    break;
                case SWT.ARROW_LEFT:
                    moveCaretByDir(SWT.LEFT, select);
                    break;
                case SWT.ARROW_RIGHT:
                    moveCaretByDir(SWT.RIGHT, select);
                    break;
                case SWT.ARROW_UP:
                    moveCaretByDir(SWT.UP, select);
                    break;
                case SWT.ARROW_DOWN:
                    moveCaretByDir(SWT.DOWN, select);
                    break;
            }
        }

        private boolean isShiftEnabled(KeyEvent e) {
            return (e.stateMask & SWT.SHIFT) != 0;
        }

        private void moveCaretByDir(int dir, boolean select) {
            if (!select) {
                cancelSelection();
            }
            moveCaretByDirection(dir, select);
        }

        @Override
        public void keyReleased(KeyEvent e) {
            if (e.keyCode == SWT.SHIFT) {
                disableShift();
            }
        }

        private void enableShift() {
            shiftEnabled = true;
        }

        private void disableShift() {
            shiftEnabled = false;
        }

        @Override
        public void focusGained(FocusEvent e) {
            text.showCaret();
        }

        @Override
        public void focusLost(FocusEvent e) {
            text.hideCaret();
        }

        private int previousHeight;

        @Override
        public void paintControl(PaintEvent e) {
            ScrolledComposite base = text.base;
            Canvas textarea = text.textarea;
            TextLayout layout = text.layout;
            Point margin = text.margin;

            int width = base.getClientArea().width - 2 * margin.x;
            layout.setWidth(width);
            layout.draw(e.gc, margin.x, margin.y);

            for (ReplaceEntry entry : text.entries) {
                entry.draw(e.gc);
            }

            int height = getHeight();

            if (height != previousHeight) {
                Rectangle bounds = new Rectangle(0, 0, width, height);
                textarea.setBounds(bounds);
                base.setMinHeight(bounds.height);
                previousHeight = height;
            }
        }

        private int getHeight() {
            TextLayout layout = text.layout;

            int lineCount = layout.getLineCount();
            int height = 0;

            for (int i = 0; i < lineCount; i++) {
                height += layout.getLineBounds(i).height;
            }

            return height;
        }

        @Override
        public void controlMoved(ControlEvent e) {
            /* empty */
        }

        @Override
        public void controlResized(ControlEvent e) {
            int offset = UNINDEXED;
            if (endSelectionIndex == UNINDEXED) {
                offset = startSelectionIndex;
            } else {
                offset = endSelectionIndex;
            }

            if (offset == UNINDEXED) {
                return;
            }
            caretOffset = offset;
            moveCaret();
        }

        @Override
        public void widgetDisposed(DisposeEvent e) {
            // 画像の破棄
            for (ReplaceEntry entry : entries) {
                entry.dispose();
            }

            // タイマー終了
            if (timer != null) {
                timer.cancel();
            }
        }
    }

    public static final void main(String[] args) {
        Display d = new Display();
        Shell shell = new Shell(d, SWT.DOUBLE_BUFFERED | SWT.TITLE | SWT.CLOSE | SWT.RESIZE);
        shell.setLayout(new FillLayout());
        LayoutText text = new LayoutText(shell, SWT.BORDER);

        text.setText("hogehoge" + REPLACEMENT
        + "fugafugaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        // Image image = new Image(d, LayoutText.class.getClassLoader().getResourceAsStream("tray.png"));

        // text.setImageEntry(image, 0);
        ImageLoader loader = new ImageLoader();
        loader.load(LayoutText.class.getClassLoader().getResourceAsStream("246.gif"));

        text.setAnimation(loader, 0);
        shell.open();

        while (!shell.isDisposed()) {
            if (!d.readAndDispatch()) {
                d.sleep();
            }
        }

        // image.dispose();
        d.dispose();
    }
}
